package controlService;

import models.*;

import java.sql.Timestamp;
import java.util.*;
import java.util.concurrent.*;

import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.LocalDevice;
import javax.microedition.io.*;

import models.Command.CommandValue;

public final class ControlService {
	public static final ControlService CS = new ControlService();

	private BlockingQueue<CommandSource> _commandQueue;
	private BlockingQueue<Command> _processedCommands;
	private List<RemoteClient> _clients;
	private Thread _updaterThread;
	private Thread _serverThread;
	private Date _lastUpdated;
	
	// This is a Singleton class!  Access it statically via ControlService.CS
	private ControlService()
	{
		_commandQueue = new LinkedBlockingQueue<CommandSource>();
		_processedCommands = new LinkedBlockingQueue<Command>();
		_clients = new ArrayList<RemoteClient>();
		_lastUpdated = new Date();
		// don't need to do anything special here
	}
	
	public void start()
	{		
		// This just spins off all of the necessary process loops as separate threads. They have to run 
		// separately or the main web site thread will block and no connections can be made.
		
		Runnable updater = new Runnable() {
			public void run() {
				processCommands();
			}
		};
				
		_updaterThread = new Thread(updater);
		_updaterThread.start();
		
		Runnable server = new Runnable() { 
			public void run()
			{
				bluetoothServer();
			}
		};
		
		_serverThread = new Thread(server);
		_serverThread.start();				
	}
	
	public int getConnectedClientCount() {
		return _clients.size();
	}
	
	public List<Command> getCommands()
	{
		syncDatabase();
		List<Command> commands = new LinkedList<Command>();
		
		synchronized(_processedCommands)
		{
			_processedCommands.drainTo(commands);
		}
		
		return commands;
	}
	
	private void bluetoothServer()
	{
		StreamConnectionNotifier notifier;
		StreamConnection connection = null;
		LocalDevice local = null;
		
		try {
			// Get reference to an actual Bluetooth device
			local = LocalDevice.getLocalDevice();
			local.setDiscoverable(DiscoveryAgent.GIAC);

			// Not sure about this stuff exactly.  See http://luugiathuy.com/2011/02/android-java-bluetooth/ for example
			javax.bluetooth.UUID uuid = new javax.bluetooth.UUID(80087355); // "04c6093b-0000-1000-8000-00805f9b34fb"
			String url = "btspp://localhost:" + uuid.toString() + ";name=RemoteBluetooth";
			System.out.println("Listening at: " + url);
			notifier = (StreamConnectionNotifier)Connector.open(url);
		} catch (Exception e) {
			System.err.println("Could not connect to bluetooth device, cannot start server.  Bailing.");
			e.printStackTrace();
			return;
		}
		
		// Here's the main loop - wait for connections and spin off threads:
		while(true) {
			try {
               	connection = notifier.acceptAndOpen();
               	
               	System.out.println("Android client connected!");
               
               	RemoteClient client = new RemoteClient(_commandQueue, connection);
               	_clients.add(client);   	
               	
				Thread remoteClientThread = new Thread(client);
				remoteClientThread.start();
			} catch (Exception e) {
				System.err.println("Error encountered while accepting new Bluetooth connection:");
				e.printStackTrace();
				return;
			}
		}			
	}
	
	private void processCommands()
	{
		while(true)	{
			try {
				CommandSource cs = _commandQueue.take();
				Command cmd = cs.command();
				RemoteClient source = cs.source();
				
				// Any communication that needs to take place between the control service and the
				// other android tablets should take place here:
				
				switch(cmd.getCommand())
				{
				case RetrieveAllTcards:
					Command request = new Command(CommandValue.RetrieveAllTcards, TcardDB.DB.getAll());
					source.enqueueOutgoing(request);
					break;
				case TcardUpdate:
					if(cmd.getTcards().size() > 0)
					{
						// Saves cards to the database - this is crucial.		
						for(Tcard tcard : cmd.getTcards())
						{
							_lastUpdated = tcard.getModified();
							TcardDB.DB.save(tcard);
						}
						
						// Send the updates to other tablets so they can see them
						for(RemoteClient client : _clients)
						{
							if(!source.equals(client))
							{
								client.enqueueOutgoing(cmd);
							}
						}
						
						synchronized(_processedCommands) { _processedCommands.add(cmd); }
					}	
					break;
				case TcardDelete:
					for(Tcard tcard : cmd.getTcards())
						TcardDB.DB.delete(tcard);
					
					synchronized(_processedCommands) { _processedCommands.add(cmd); }
					break;
				case CloseConnection:
					_clients.remove(source);
					synchronized(_processedCommands) { _processedCommands.add(cmd); }
					break;
				default:		
					synchronized(_processedCommands) { _processedCommands.add(cmd); }
					break;
				}
				
			}
			catch(Exception e) {
				e.printStackTrace();
			}
		}
		
	}
	
	// Finds any updates in the database not directly entered by an Android tablet
	// Mostly useful for debugging purposes...
	private void syncDatabase()
	{
		Collection<Tcard> updates = TcardDB.DB.getSince(_lastUpdated);

		if(updates.size() > 0)
		{
			_lastUpdated = new Date();			
			Command cmd = new Command(CommandValue.TcardUpdate, updates);
			
			synchronized(_processedCommands)
			{
				_processedCommands.add(cmd);
			}
			
			for(RemoteClient client : _clients)
				client.enqueueOutgoing(cmd);
		}		
	}
}
